/*
 * Copyright (c) 1997, 2011, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

/*
 * (C) Copyright Taligent, Inc. 1996 - 1997, All Rights Reserved
 * (C) Copyright IBM Corp. 1996 - 1998, All Rights Reserved
 *
 * The original version of this source code and documentation is
 * copyrighted and owned by Taligent, Inc., a wholly-owned subsidiary
 * of IBM. These materials are provided under terms of a License
 * Agreement between Taligent and Sun. This technology is protected
 * by multiple US and International patents.
 *
 * This notice and attribution to Taligent may not be removed.
 * Taligent is a registered trademark of Taligent, Inc.
 *
 */

package java.awt.font;

import java.awt.Font;

import java.text.AttributedCharacterIterator;
import java.text.AttributedCharacterIterator.Attribute;
import java.text.AttributedString;
import java.text.Bidi;
import java.text.BreakIterator;
import java.text.CharacterIterator;

import java.awt.font.FontRenderContext;

import java.util.Hashtable;
import java.util.Map;

import sun.font.AttributeValues;
import sun.font.BidiUtils;
import sun.font.TextLineComponent;
import sun.font.TextLabelFactory;
import sun.font.FontResolver;

/**
 * The <code>TextMeasurer</code> class provides the primitive operations
 * needed for line break: measuring up to a given advance, determining the
 * advance of a range of characters, and generating a
 * <code>TextLayout</code> for a range of characters. It also provides
 * methods for incremental editing of paragraphs.
 * <p>
 * A <code>TextMeasurer</code> object is constructed with an
 * {@link java.text.AttributedCharacterIterator AttributedCharacterIterator}
 * representing a single paragraph of text.  The value returned by the
 * {@link AttributedCharacterIterator#getBeginIndex() getBeginIndex}
 * method of <code>AttributedCharacterIterator</code>
 * defines the absolute index of the first character.  The value
 * returned by the
 * {@link AttributedCharacterIterator#getEndIndex() getEndIndex}
 * method of <code>AttributedCharacterIterator</code> defines the index
 * past the last character.  These values define the range of indexes to
 * use in calls to the <code>TextMeasurer</code>.  For example, calls to
 * get the advance of a range of text or the line break of a range of text
 * must use indexes between the beginning and end index values.  Calls to
 * {@link #insertChar(java.text.AttributedCharacterIterator, int) insertChar}
 * and
 * {@link #deleteChar(java.text.AttributedCharacterIterator, int) deleteChar}
 * reset the <code>TextMeasurer</code> to use the beginning index and end
 * index of the <code>AttributedCharacterIterator</code> passed in those calls.
 * <p>
 * Most clients will use the more convenient <code>LineBreakMeasurer</code>,
 * which implements the standard line break policy (placing as many words
 * as will fit on each line).
 *
 * @author John Raley
 * @see LineBreakMeasurer
 * @since 1.3
 */

public final class TextMeasurer implements Cloneable {

  // Number of lines to format to.
  private static float EST_LINES = (float) 2.1;

    /*
    static {
        String s = System.getProperty("estLines");
        if (s != null) {
            try {
                Float f = new Float(s);
                EST_LINES = f.floatValue();
            }
            catch(NumberFormatException e) {
            }
        }
        //System.out.println("EST_LINES="+EST_LINES);
    }
    */

  private FontRenderContext fFrc;

  private int fStart;

  // characters in source text
  private char[] fChars;

  // Bidi for this paragraph
  private Bidi fBidi;

  // Levels array for chars in this paragraph - needed to reorder
  // trailing counterdirectional whitespace
  private byte[] fLevels;

  // line components in logical order
  private TextLineComponent[] fComponents;

  // index where components begin
  private int fComponentStart;

  // index where components end
  private int fComponentLimit;

  private boolean haveLayoutWindow;

  // used to find valid starting points for line components
  private BreakIterator fLineBreak = null;
  private CharArrayIterator charIter = null;
  int layoutCount = 0;
  int layoutCharCount = 0;

  // paragraph, with resolved fonts and styles
  private StyledParagraph fParagraph;

  // paragraph data - same across all layouts
  private boolean fIsDirectionLTR;
  private byte fBaseline;
  private float[] fBaselineOffsets;
  private float fJustifyRatio = 1;

  /**
   * Constructs a <code>TextMeasurer</code> from the source text.
   * The source text should be a single entire paragraph.
   *
   * @param text the source paragraph.  Cannot be null.
   * @param frc the information about a graphics device which is needed to measure the text
   * correctly.  Cannot be null.
   */
  public TextMeasurer(AttributedCharacterIterator text, FontRenderContext frc) {

    fFrc = frc;
    initAll(text);
  }

  protected Object clone() {
    TextMeasurer other;
    try {
      other = (TextMeasurer) super.clone();
    } catch (CloneNotSupportedException e) {
      throw new Error();
    }
    if (fComponents != null) {
      other.fComponents = fComponents.clone();
    }
    return other;
  }

  private void invalidateComponents() {
    fComponentStart = fComponentLimit = fChars.length;
    fComponents = null;
    haveLayoutWindow = false;
  }

  /**
   * Initialize state, including fChars array, direction, and
   * fBidi.
   */
  private void initAll(AttributedCharacterIterator text) {

    fStart = text.getBeginIndex();

    // extract chars
    fChars = new char[text.getEndIndex() - fStart];

    int n = 0;
    for (char c = text.first();
        c != CharacterIterator.DONE;
        c = text.next()) {
      fChars[n++] = c;
    }

    text.first();

    fBidi = new Bidi(text);
    if (fBidi.isLeftToRight()) {
      fBidi = null;
    }

    text.first();
    Map<? extends Attribute, ?> paragraphAttrs = text.getAttributes();
    NumericShaper shaper = AttributeValues.getNumericShaping(paragraphAttrs);
    if (shaper != null) {
      shaper.shape(fChars, 0, fChars.length);
    }

    fParagraph = new StyledParagraph(text, fChars);

    // set paragraph attributes
    {
      // If there's an embedded graphic at the start of the
      // paragraph, look for the first non-graphic character
      // and use it and its font to initialize the paragraph.
      // If not, use the first graphic to initialize.
      fJustifyRatio = AttributeValues.getJustification(paragraphAttrs);

      boolean haveFont = TextLine.advanceToFirstFont(text);

      if (haveFont) {
        Font defaultFont = TextLine.getFontAtCurrentPos(text);
        int charsStart = text.getIndex() - text.getBeginIndex();
        LineMetrics lm = defaultFont.getLineMetrics(fChars, charsStart, charsStart + 1, fFrc);
        fBaseline = (byte) lm.getBaselineIndex();
        fBaselineOffsets = lm.getBaselineOffsets();
      } else {
        // hmmm what to do here?  Just try to supply reasonable
        // values I guess.

        GraphicAttribute graphic = (GraphicAttribute)
            paragraphAttrs.get(TextAttribute.CHAR_REPLACEMENT);
        fBaseline = TextLayout.getBaselineFromGraphic(graphic);
        Hashtable<Attribute, ?> fmap = new Hashtable<>(5, (float) 0.9);
        Font dummyFont = new Font(fmap);
        LineMetrics lm = dummyFont.getLineMetrics(" ", 0, 1, fFrc);
        fBaselineOffsets = lm.getBaselineOffsets();
      }
      fBaselineOffsets = TextLine.getNormalizedOffsets(fBaselineOffsets, fBaseline);
    }

    invalidateComponents();
  }

  /**
   * Generate components for the paragraph.  fChars, fBidi should have been
   * initialized already.
   */
  private void generateComponents(int startingAt, int endingAt) {

    if (collectStats) {
      formattedChars += (endingAt - startingAt);
    }
    int layoutFlags = 0; // no extra info yet, bidi determines run and line direction
    TextLabelFactory factory = new TextLabelFactory(fFrc, fChars, fBidi, layoutFlags);

    int[] charsLtoV = null;

    if (fBidi != null) {
      fLevels = BidiUtils.getLevels(fBidi);
      int[] charsVtoL = BidiUtils.createVisualToLogicalMap(fLevels);
      charsLtoV = BidiUtils.createInverseMap(charsVtoL);
      fIsDirectionLTR = fBidi.baseIsLeftToRight();
    } else {
      fLevels = null;
      fIsDirectionLTR = true;
    }

    try {
      fComponents = TextLine.getComponents(
          fParagraph, fChars, startingAt, endingAt, charsLtoV, fLevels, factory);
    } catch (IllegalArgumentException e) {
      System.out.println("startingAt=" + startingAt + "; endingAt=" + endingAt);
      System.out.println("fComponentLimit=" + fComponentLimit);
      throw e;
    }

    fComponentStart = startingAt;
    fComponentLimit = endingAt;
    //debugFormatCount += (endingAt-startingAt);
  }

  private int calcLineBreak(final int pos, final float maxAdvance) {

    // either of these statements removes the bug:
    //generateComponents(0, fChars.length);
    //generateComponents(pos, fChars.length);

    int startPos = pos;
    float width = maxAdvance;

    int tlcIndex;
    int tlcStart = fComponentStart;

    for (tlcIndex = 0; tlcIndex < fComponents.length; tlcIndex++) {
      int gaLimit = tlcStart + fComponents[tlcIndex].getNumCharacters();
      if (gaLimit > startPos) {
        break;
      } else {
        tlcStart = gaLimit;
      }
    }

    // tlcStart is now the start of the tlc at tlcIndex

    for (; tlcIndex < fComponents.length; tlcIndex++) {

      TextLineComponent tlc = fComponents[tlcIndex];
      int numCharsInGa = tlc.getNumCharacters();

      int lineBreak = tlc.getLineBreakIndex(startPos - tlcStart, width);
      if (lineBreak == numCharsInGa && tlcIndex < fComponents.length) {
        width -= tlc.getAdvanceBetween(startPos - tlcStart, lineBreak);
        tlcStart += numCharsInGa;
        startPos = tlcStart;
      } else {
        return tlcStart + lineBreak;
      }
    }

    if (fComponentLimit < fChars.length) {
      // format more text and try again
      //if (haveLayoutWindow) {
      //    outOfWindow++;
      //}

      generateComponents(pos, fChars.length);
      return calcLineBreak(pos, maxAdvance);
    }

    return fChars.length;
  }

  /**
   * According to the Unicode Bidirectional Behavior specification
   * (Unicode Standard 2.0, section 3.11), whitespace at the ends
   * of lines which would naturally flow against the base direction
   * must be made to flow with the line direction, and moved to the
   * end of the line.  This method returns the start of the sequence
   * of trailing whitespace characters to move to the end of a
   * line taken from the given range.
   */
  private int trailingCdWhitespaceStart(int startPos, int limitPos) {

    if (fLevels != null) {
      // Back up over counterdirectional whitespace
      final byte baseLevel = (byte) (fIsDirectionLTR ? 0 : 1);
      for (int cdWsStart = limitPos; --cdWsStart >= startPos; ) {
        if ((fLevels[cdWsStart] % 2) == baseLevel ||
            Character.getDirectionality(fChars[cdWsStart]) != Character.DIRECTIONALITY_WHITESPACE) {
          return ++cdWsStart;
        }
      }
    }

    return startPos;
  }

  private TextLineComponent[] makeComponentsOnRange(int startPos,
      int limitPos) {

    // sigh I really hate to do this here since it's part of the
    // bidi algorithm.
    // cdWsStart is the start of the trailing counterdirectional
    // whitespace
    final int cdWsStart = trailingCdWhitespaceStart(startPos, limitPos);

    int tlcIndex;
    int tlcStart = fComponentStart;

    for (tlcIndex = 0; tlcIndex < fComponents.length; tlcIndex++) {
      int gaLimit = tlcStart + fComponents[tlcIndex].getNumCharacters();
      if (gaLimit > startPos) {
        break;
      } else {
        tlcStart = gaLimit;
      }
    }

    // tlcStart is now the start of the tlc at tlcIndex

    int componentCount;
    {
      boolean split = false;
      int compStart = tlcStart;
      int lim = tlcIndex;
      for (boolean cont = true; cont; lim++) {
        int gaLimit = compStart + fComponents[lim].getNumCharacters();
        if (cdWsStart > Math.max(compStart, startPos)
            && cdWsStart < Math.min(gaLimit, limitPos)) {
          split = true;
        }
        if (gaLimit >= limitPos) {
          cont = false;
        } else {
          compStart = gaLimit;
        }
      }
      componentCount = lim - tlcIndex;
      if (split) {
        componentCount++;
      }
    }

    TextLineComponent[] components = new TextLineComponent[componentCount];
    int newCompIndex = 0;
    int linePos = startPos;

    int breakPt = cdWsStart;

    int subsetFlag;
    if (breakPt == startPos) {
      subsetFlag = fIsDirectionLTR ? TextLineComponent.LEFT_TO_RIGHT :
          TextLineComponent.RIGHT_TO_LEFT;
      breakPt = limitPos;
    } else {
      subsetFlag = TextLineComponent.UNCHANGED;
    }

    while (linePos < limitPos) {

      int compLength = fComponents[tlcIndex].getNumCharacters();
      int tlcLimit = tlcStart + compLength;

      int start = Math.max(linePos, tlcStart);
      int limit = Math.min(breakPt, tlcLimit);

      components[newCompIndex++] = fComponents[tlcIndex].getSubset(
          start - tlcStart,
          limit - tlcStart,
          subsetFlag);
      linePos += (limit - start);
      if (linePos == breakPt) {
        breakPt = limitPos;
        subsetFlag = fIsDirectionLTR ? TextLineComponent.LEFT_TO_RIGHT :
            TextLineComponent.RIGHT_TO_LEFT;
      }
      if (linePos == tlcLimit) {
        tlcIndex++;
        tlcStart = tlcLimit;
      }
    }

    return components;
  }

  private TextLine makeTextLineOnRange(int startPos, int limitPos) {

    int[] charsLtoV = null;
    byte[] charLevels = null;

    if (fBidi != null) {
      Bidi lineBidi = fBidi.createLineBidi(startPos, limitPos);
      charLevels = BidiUtils.getLevels(lineBidi);
      int[] charsVtoL = BidiUtils.createVisualToLogicalMap(charLevels);
      charsLtoV = BidiUtils.createInverseMap(charsVtoL);
    }

    TextLineComponent[] components = makeComponentsOnRange(startPos, limitPos);

    return new TextLine(fFrc,
        components,
        fBaselineOffsets,
        fChars,
        startPos,
        limitPos,
        charsLtoV,
        charLevels,
        fIsDirectionLTR);

  }

  private void ensureComponents(int start, int limit) {

    if (start < fComponentStart || limit > fComponentLimit) {
      generateComponents(start, limit);
    }
  }

  private void makeLayoutWindow(int localStart) {

    int compStart = localStart;
    int compLimit = fChars.length;

    // If we've already gone past the layout window, format to end of paragraph
    if (layoutCount > 0 && !haveLayoutWindow) {
      float avgLineLength = Math.max(layoutCharCount / layoutCount, 1);
      compLimit = Math.min(localStart + (int) (avgLineLength * EST_LINES), fChars.length);
    }

    if (localStart > 0 || compLimit < fChars.length) {
      if (charIter == null) {
        charIter = new CharArrayIterator(fChars);
      } else {
        charIter.reset(fChars);
      }
      if (fLineBreak == null) {
        fLineBreak = BreakIterator.getLineInstance();
      }
      fLineBreak.setText(charIter);
      if (localStart > 0) {
        if (!fLineBreak.isBoundary(localStart)) {
          compStart = fLineBreak.preceding(localStart);
        }
      }
      if (compLimit < fChars.length) {
        if (!fLineBreak.isBoundary(compLimit)) {
          compLimit = fLineBreak.following(compLimit);
        }
      }
    }

    ensureComponents(compStart, compLimit);
    haveLayoutWindow = true;
  }

  /**
   * Returns the index of the first character which will not fit on
   * on a line beginning at <code>start</code> and possible
   * measuring up to <code>maxAdvance</code> in graphical width.
   *
   * @param start the character index at which to start measuring. <code>start</code> is an absolute
   * index, not relative to the start of the paragraph
   * @param maxAdvance the graphical width in which the line must fit
   * @return the index after the last character that will fit on a line beginning at
   * <code>start</code>, which is not longer than <code>maxAdvance</code> in graphical width
   * @throws IllegalArgumentException if <code>start</code> is less than the beginning of the
   * paragraph.
   */
  public int getLineBreakIndex(int start, float maxAdvance) {

    int localStart = start - fStart;

    if (!haveLayoutWindow ||
        localStart < fComponentStart ||
        localStart >= fComponentLimit) {
      makeLayoutWindow(localStart);
    }

    return calcLineBreak(localStart, maxAdvance) + fStart;
  }

  /**
   * Returns the graphical width of a line beginning at <code>start</code>
   * and including characters up to <code>limit</code>.
   * <code>start</code> and <code>limit</code> are absolute indices,
   * not relative to the start of the paragraph.
   *
   * @param start the character index at which to start measuring
   * @param limit the character index at which to stop measuring
   * @return the graphical width of a line beginning at <code>start</code> and including characters
   * up to <code>limit</code>
   * @throws IndexOutOfBoundsException if <code>limit</code> is less than <code>start</code>
   * @throws IllegalArgumentException if <code>start</code> or <code>limit</code> is not between the
   * beginning of the paragraph and the end of the paragraph.
   */
  public float getAdvanceBetween(int start, int limit) {

    int localStart = start - fStart;
    int localLimit = limit - fStart;

    ensureComponents(localStart, localLimit);
    TextLine line = makeTextLineOnRange(localStart, localLimit);
    return line.getMetrics().advance;
    // could cache line in case getLayout is called with same start, limit
  }

  /**
   * Returns a <code>TextLayout</code> on the given character range.
   *
   * @param start the index of the first character
   * @param limit the index after the last character.  Must be greater than <code>start</code>
   * @return a <code>TextLayout</code> for the characters beginning at <code>start</code> up to (but
   * not including) <code>limit</code>
   * @throws IndexOutOfBoundsException if <code>limit</code> is less than <code>start</code>
   * @throws IllegalArgumentException if <code>start</code> or <code>limit</code> is not between the
   * beginning of the paragraph and the end of the paragraph.
   */
  public TextLayout getLayout(int start, int limit) {

    int localStart = start - fStart;
    int localLimit = limit - fStart;

    ensureComponents(localStart, localLimit);
    TextLine textLine = makeTextLineOnRange(localStart, localLimit);

    if (localLimit < fChars.length) {
      layoutCharCount += limit - start;
      layoutCount++;
    }

    return new TextLayout(textLine,
        fBaseline,
        fBaselineOffsets,
        fJustifyRatio);
  }

  private int formattedChars = 0;
  private static boolean wantStats = false;/*"true".equals(System.getProperty("collectStats"));*/
  private boolean collectStats = false;

  private void printStats() {
    System.out.println("formattedChars: " + formattedChars);
    //formattedChars = 0;
    collectStats = false;
  }

  /**
   * Updates the <code>TextMeasurer</code> after a single character has
   * been inserted
   * into the paragraph currently represented by this
   * <code>TextMeasurer</code>.  After this call, this
   * <code>TextMeasurer</code> is equivalent to a new
   * <code>TextMeasurer</code> created from the text;  however, it will
   * usually be more efficient to update an existing
   * <code>TextMeasurer</code> than to create a new one from scratch.
   *
   * @param newParagraph the text of the paragraph after performing the insertion.  Cannot be null.
   * @param insertPos the position in the text where the character was inserted.  Must not be less
   * than the start of <code>newParagraph</code>, and must be less than the end of
   * <code>newParagraph</code>.
   * @throws IndexOutOfBoundsException if <code>insertPos</code> is less than the start of
   * <code>newParagraph</code> or greater than or equal to the end of <code>newParagraph</code>
   * @throws NullPointerException if <code>newParagraph</code> is <code>null</code>
   */
  public void insertChar(AttributedCharacterIterator newParagraph, int insertPos) {

    if (collectStats) {
      printStats();
    }
    if (wantStats) {
      collectStats = true;
    }

    fStart = newParagraph.getBeginIndex();
    int end = newParagraph.getEndIndex();
    if (end - fStart != fChars.length + 1) {
      initAll(newParagraph);
    }

    char[] newChars = new char[end - fStart];
    int newCharIndex = insertPos - fStart;
    System.arraycopy(fChars, 0, newChars, 0, newCharIndex);

    char newChar = newParagraph.setIndex(insertPos);
    newChars[newCharIndex] = newChar;
    System.arraycopy(fChars,
        newCharIndex,
        newChars,
        newCharIndex + 1,
        end - insertPos - 1);
    fChars = newChars;

    if (fBidi != null || Bidi.requiresBidi(newChars, newCharIndex, newCharIndex + 1) ||
        newParagraph.getAttribute(TextAttribute.BIDI_EMBEDDING) != null) {

      fBidi = new Bidi(newParagraph);
      if (fBidi.isLeftToRight()) {
        fBidi = null;
      }
    }

    fParagraph = StyledParagraph.insertChar(newParagraph,
        fChars,
        insertPos,
        fParagraph);
    invalidateComponents();
  }

  /**
   * Updates the <code>TextMeasurer</code> after a single character has
   * been deleted
   * from the paragraph currently represented by this
   * <code>TextMeasurer</code>.  After this call, this
   * <code>TextMeasurer</code> is equivalent to a new <code>TextMeasurer</code>
   * created from the text;  however, it will usually be more efficient
   * to update an existing <code>TextMeasurer</code> than to create a new one
   * from scratch.
   *
   * @param newParagraph the text of the paragraph after performing the deletion.  Cannot be null.
   * @param deletePos the position in the text where the character was removed. Must not be less
   * than the start of <code>newParagraph</code>, and must not be greater than the end of
   * <code>newParagraph</code>.
   * @throws IndexOutOfBoundsException if <code>deletePos</code> is less than the start of
   * <code>newParagraph</code> or greater than the end of <code>newParagraph</code>
   * @throws NullPointerException if <code>newParagraph</code> is <code>null</code>
   */
  public void deleteChar(AttributedCharacterIterator newParagraph, int deletePos) {

    fStart = newParagraph.getBeginIndex();
    int end = newParagraph.getEndIndex();
    if (end - fStart != fChars.length - 1) {
      initAll(newParagraph);
    }

    char[] newChars = new char[end - fStart];
    int changedIndex = deletePos - fStart;

    System.arraycopy(fChars, 0, newChars, 0, deletePos - fStart);
    System.arraycopy(fChars, changedIndex + 1, newChars, changedIndex, end - deletePos);
    fChars = newChars;

    if (fBidi != null) {
      fBidi = new Bidi(newParagraph);
      if (fBidi.isLeftToRight()) {
        fBidi = null;
      }
    }

    fParagraph = StyledParagraph.deleteChar(newParagraph,
        fChars,
        deletePos,
        fParagraph);
    invalidateComponents();
  }

  /**
   * NOTE:  This method is only for LineBreakMeasurer's use.  It is package-
   * private because it returns internal data.
   */
  char[] getChars() {

    return fChars;
  }
}
